﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml.Linq;

namespace Foundation.Spell
{
    internal class SpellChecker
    {
        public static readonly Regex DefaultWholeWordRegex = new Regex(@"\b\w+\b", RegexOptions.Compiled);  
        private readonly Stack<string> _checkStack = new Stack<string>();         
        private readonly EventWaitHandle _checkEvent = new ManualResetEvent(true);
        private readonly SortedList<string, string[]> _dictionary = new SortedList<string, string[]>();

        public SpellChecker(SpellService webService = null, SortedList<string, string[]> dictionary = null)
        {
            WebService = webService;
            ExceptionStack = new Stack<Exception>();
            _dictionary = dictionary ?? _dictionary;
            if (WebService != null) new Thread(Check) {IsBackground = true, Priority = ThreadPriority.Lowest}.Start();
            WholeWordRegex =
                WebService != null && !string.IsNullOrEmpty(WebService.WholeWordRegexPattern)
                    ? new Regex(WebService.WholeWordRegexPattern, RegexOptions.Compiled)
                    : DefaultWholeWordRegex;
            TryUseWebService = true;
        }

        public event Action<string[]> NewWordsChecked = words => { };   
        public Stack<Exception> ExceptionStack { get; private set; }
        public SpellService WebService { get; private set; }
        public Regex WholeWordRegex { get; private set; }       
        public bool TryUseWebService { get; set; }

        public void SetAssotiatedWords(string word, string[] assotiatedWords)
        {
            if (!_dictionary.ContainsKey(word)) _dictionary.Add(word, null); // add new word to dictionary
            _dictionary[word] = assotiatedWords; // set assotiations
            if (assotiatedWords == null) return; // it is right word (no assotiations - return)

            for (var i = 0; i < assotiatedWords.Length; i++) 
            {
                var assotiation = assotiatedWords[i];
                var index = _dictionary.IndexOfKey(assotiation);
                if (index < 0) _dictionary.Add(assotiation, null); // add new word to dictionary
                else assotiatedWords[i] = _dictionary.Keys[index]; // replace assotiations strings to existing words for optimal memory use
            }
        }

        public List<SpellAssotiation> Check(string text, out string[] newWords)
        {
            var wordMatches = WholeWordRegex.Matches(text).Cast<Match>().ToArray();
            var uniqueWords = wordMatches.Select(match => match.Value).Distinct().ToArray();
            newWords = uniqueWords.Where(word => !_dictionary.ContainsKey(word)).ToArray();
            if (WebService != null && newWords.Any() && TryUseWebService)
            {
                var line = newWords.Aggregate((a, b) => a + " " + b);
                _checkStack.Push(line);
                _checkEvent.Set();
            }

            string[] assotiatedWords = null;
            return (from wordMatch in wordMatches
                    let word = wordMatch.Value
                    where _dictionary.TryGetValue(word, out assotiatedWords) && assotiatedWords != null
                    select new SpellAssotiation
                        {
                            Word = word,
                            Offset = wordMatch.Index,
                            AssotiatedWords = assotiatedWords,
                        }).ToList(); // return error infos
        }

        private void Check()
        {
            do
            {
                var textForCheck = string.Empty;

                try
                {
                    if (_checkStack.Count == 0 || !TryUseWebService) _checkEvent.Reset();
                    _checkEvent.WaitOne();

                    while (_checkStack.Count > 0)
                    {
                        var line = _checkStack.Pop();
                        var wordMatches = WholeWordRegex.Matches(line).Cast<Match>().ToList();
                        var uniqueWords = wordMatches.Select(match => match.Value).Distinct().ToList();
                        var newWords = uniqueWords.Where(word => !_dictionary.ContainsKey(word) && !textForCheck.Contains(word)).ToList();
                        if (!newWords.Any()) continue;
                        textForCheck += newWords.Aggregate((a, b) => a + " " + b) + " ";
                        if (textForCheck.Length >= WebService.MinQueryStringLenght) break;
                    }

                    if (string.IsNullOrWhiteSpace(textForCheck)) continue;

                    var responce =
                        string.IsNullOrEmpty(WebService.Get) || textForCheck.Length > WebService.MaxGetQueryStringLenght
                            ? WebHelper.HttpPost(WebService.Url, WebService.ContentType, string.Format(WebService.Post, textForCheck))
                            : WebHelper.HttpGet(WebService.Url + "?" + string.Format(WebService.Get, textForCheck)); // wait responce

                    if (string.IsNullOrWhiteSpace(responce)) continue;
                    
                    var xmlElements = XElement.Parse(responce);
                    var getAssotiatedWords = // function for geting suggested words from xml element
                        string.IsNullOrEmpty(WebService.HintNode)
                            ? (Func<XElement, string[]>)(e => e.Value.Split('\t').ToArray())
                            : (e => e.Descendants(WebService.HintNode).Select(n => n.Value).ToArray());

                    var spellAssotiations =
                        (from xmlElement in xmlElements.Descendants(WebService.ErrorNodeName)
                         let offsetAttribute = xmlElement.Attribute(WebService.OffsetAttributeName)
                         let lenghtAttribute = xmlElement.Attribute(WebService.LenghtAttributeName)
                         where lenghtAttribute != null && offsetAttribute != null &&
                               !(WebService.IgnoreEmptyErrors && string.IsNullOrEmpty(xmlElement.Value))
                         let offset = int.Parse(offsetAttribute.Value) // word offset in textForCheck
                         let lenght = int.Parse(lenghtAttribute.Value) // word lenght in textForCheck
                         select new SpellAssotiation
                             {
                                 Offset = offset,
                                 Word = textForCheck.Substring(offset, lenght),
                                 AssotiatedWords = getAssotiatedWords(xmlElement)
                             }).ToList(); // get error infos from web-responce

                    var words = textForCheck.Split(); // checked words
                    foreach (var word in words)
                    {
                        if (!_dictionary.ContainsKey(word)) lock (_dictionary) _dictionary.Add(word, null); // add checked word to dictionary
                        var spellAssotiation = spellAssotiations.FirstOrDefault(spe => spe.Word == word); // find error info for wrong word
                        if (spellAssotiation != null) lock (_dictionary) SetAssotiatedWords(word, spellAssotiation.AssotiatedWords);
                    }

                    NewWordsChecked(words); // notify when new words checked
                    if (WebService.QueryTimeout > 0) Thread.Sleep(WebService.QueryTimeout); // protection timeout from DDOS attack
                }
                catch (Exception exception)
                {
                    _checkStack.Push(textForCheck);
                    ExceptionStack.Push(exception);
                }
            } while (_checkEvent != null);
        }
    }
}